#pragma once

#include <functional>
#include <iostream>
#include <string>

#include "TermVisitor.hpp"

namespace terms {
	using std::size_t;

	/**
	 * \brief Base class for all Terms
	 *
	 * This is the base class with the methods that all Term
	 * implementations must implement. There is only very limited
	 * functionality here, making this more of an interface class.
	 *
	 * Concrete term systems should not be derived from \ref Term directly.
	 * Instead, each term system should establish its own base term
	 * by deriving from \ref TermSystem. See there, for details.
	 *
	 * Terms need to be immutable singletons. See \ref TermFactory.
	 **/
	class Term {
	public:
		virtual ~Term()
		{
		}

	public:
		/**
		 * Calculate a hash value for the Term.
		 **/
		virtual size_t hash() const = 0;

	public:
		/**
		 * \brief Test, if a term is atomic.
		 *
		 * Overriden by \ref Atom. You should never override
		 * this function yourself, but instead simply inherit from Atom.
		 **/
		virtual bool is_atomic() const
		{
			return false;
		}

	public:
		/**
		 * Basic \ref TermVisitor support.
		 **/
		virtual void accept(TermVisitor<Term> &visitor) const
		{
			visitor.visitBefore(*this);
			visitor.visit(*this);
			visitor.visitAfter(*this);
		}

	public:
		/**
		 * \brief Convert the term into a string representation.
		 *
		 * The actual representation is unspecified but it should be
		 * a well known representation of the terms in use.
		 **/
		virtual std::string to_string() const = 0;
	};

}

namespace std {
	/**
	 * \brief std::hash for terms. 
	 *
	 * Forward to virtual hash method of the term.
	 **/
	template <>
	struct hash<terms::Term> {
		size_t
		operator()(const terms::Term &term) const
		{
			return term.hash();
		}
	};

	/**
	 * \brief std::to_string for terms.
	 *
	 * Forward to virtual to_string() method.
	 **/
	string to_string(const terms::Term &term);

	/**
	 * \brief Output operator for terms.
	 *
	 * Converts term to string using std::to_string() and prints the
	 * result.
	 **/
	ostream &operator <<(ostream &os, const terms::Term &term);
}
